﻿using System;
using System.Collections.Generic;
using System.Net.Sockets;

// SDD# - Software Defined Devices in C# for Linux and Windows
// Internet Availability Monitor
// (c) 2014-2015 Chris Burri. MIT License applies. 
// See http://niston.wordpress.com

namespace sdd.subsystems.network
{
    public class InternetAvailabilityTest
    {
        #region Member Variables
        // check scheduler
        private System.Timers.Timer testTimer = new System.Timers.Timer();
        // enabled flag
        private bool testEnabled = false;
        // test running flag
        private bool testRunning = false;
        // last known test time
        private DateTime lastRun;
        // last known test result
        private bool lastAvailable = false;
        // last known test result is valid
        private bool lastAvailableValid = false;
        #endregion

        #region Constructor / Destructor
        public InternetAvailabilityTest(List<string> httpServerNames = null, int timeoutTCPConnect = 5000, int intervalAvailable = 60000, int intervalUnavailable = 10000)
        {
            // assign initial values
            TimeoutTCPConnect = timeoutTCPConnect;
            HTTPServerNames = (httpServerNames != null) ? httpServerNames : new List<string>() { "www.google.com", "www.microsoft.com" }; // defaults
            TestIntervalAvailable = intervalAvailable;
            TestIntervalUnavailable = intervalUnavailable;

            // hook check scheduler event
            testTimer.Elapsed += testTimer_Elapsed;
            testTimer.AutoReset = false;
        }

        ~InternetAvailabilityTest()
        {
            testEnabled = false;
            testTimer.Elapsed -= testTimer_Elapsed;
            testTimer.Stop();
        }
        #endregion

        #region Public/Internal Interface
        // notification before starting periodic test, so that applications may report connectivity
        public event EventHandler<BeginTestEventArgs> BeginTest;

        // internet availability
        public event EventHandler<AvailabilityChangedEventArgs> AvailabilityChanged;

        // timeout value for TCP connect test
        public int TimeoutTCPConnect { set; get; }

        // test interval values (milliseconds)
        public int TestIntervalAvailable { set; get; }      // next interval after successful test run
        public int TestIntervalUnavailable { set; get; }    // next interval after failed test run

        // list of http servers (port 80) to use for testing
        public List<string> HTTPServerNames { internal set; get; }

        // availability test results
        public bool Available { private set; get; }     // current

        // application may report internet availability by calling this method
        public void ReportConnectivity()
        {
            // do not act if we already know we have connectivity
            if (!Available)
            {
                // we don't. update availability info.
                lastAvailable = Available;
                Available = true;

                // adjust interval for next scheduled test occurence
                testTimer.Interval = Available ? TestIntervalUnavailable : TestIntervalUnavailable;

                // raise availability changed event
                if (AvailabilityChanged != null) { AvailabilityChanged(this, new AvailabilityChangedEventArgs(lastAvailable, Available)); }
            }
        }

        // application may trigger a re-test by calling this method
        public void ReportConnectivityProblem()
        {
            // only act if we believe we are on-line
            if (Available)
            {
                // did the minimum interval between last check already pass?
                if ( DateTime.Now.Subtract(lastRun).TotalSeconds >= TestIntervalUnavailable )
                {
                    // yes. so: we think we have internet, but application reports trouble -> immediately perform availability test if not already running
                    if (!testRunning) { PerformInternetAvailabilityTest(); }
                }
                else
                {
                    // no. ignore this report.
                }
            }
        }

        // start performing tests
        internal void StartPeriodicTest()
        {
            testEnabled = true;
            testTimer.Start();
        }

        // stop performing tests
        internal void StopPeriodicTest()
        {
            testEnabled = false;
            testTimer.Stop();
        }

        #endregion

        #region Event Handlers
        private void testTimer_Elapsed(object sender, System.Timers.ElapsedEventArgs e)
        {
            // periodical test
            PerformInternetAvailabilityTest();
            // schedule next periodical test
            if (testEnabled) { testTimer.Start(); }
        }
        #endregion

        #region Private Methods
        private void PerformInternetAvailabilityTest()
        {
            if (testEnabled)
            {
                if (!testRunning)
                {
                    // prevent reentrancy
                    testRunning = true;

                    // raise BeginTest notification event
                    BeginTestEventArgs eArgs = new BeginTestEventArgs();
                    if (BeginTest != null) { BeginTest(this, eArgs); }

                    // see if any client application reports internet connectivity
                    if (!eArgs.IsConnected)
                    {
                        // no client application reported something. must perform HTTP tests...
                        bool netIsAvailable = PerformHTTPAvailabilityTests();

                        // currently no other tests defined, so we already have the result
                        Available = netIsAvailable;

                        // adjust reschedule interval according to test results
                        testTimer.Interval = Available ? TestIntervalUnavailable : TestIntervalUnavailable;      // shorten interval on problem detected

                    }
                    else
                    {
                        // application reports connection, assume availability and do not perform tests
                        Available = true;

                        // set reschedule interval
                        testTimer.Interval = TestIntervalAvailable;
                    }

                    // raise event if changed since last test
                    RaiseAvailabilityChangedEventOnChangeOnly();

                    // remember this test run's availability result 
                    lastRun = DateTime.Now;
                    lastAvailable = Available;
                    lastAvailableValid = true;

                    // clear reentrancy prevention flag
                    testRunning = false;
                }
            }
        }

        private void RaiseAvailabilityChangedEventOnChangeOnly()
        {
            // detect change since last test, always detect on first test
            if ((Available != lastAvailable) || (lastAvailable == false))
            {
                // raise AvailabilityChanged event
                if (AvailabilityChanged != null)
                {
                    AvailabilityChanged(this, new AvailabilityChangedEventArgs(lastAvailable, Available));
                }
            }
        }

        // check for availability of well-known HTTP internet services
        private bool PerformHTTPAvailabilityTests()
        {
            // out test socket
            Socket socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);

            // our test result, assumed to be Fail
            bool overallResult = false;

            // try each server name in list until connection succeeds or names run out
            foreach (string serverName in HTTPServerNames)
            {
                try
                {
                    // attempt TCP connection to port 80
                    IAsyncResult result = socket.BeginConnect(serverName, 80, null, null);
                    if (result.AsyncWaitHandle.WaitOne(TimeoutTCPConnect, true))
                    {
                        // success
                        overallResult = true;
                        // exit the loop
                        break;
                    }
                }
                catch (Exception ex) { } // blissfully ignore, TODO: parse exception for network status indicators
                finally
                {
                    // close the socket!
                    socket.Close();
                }
            }

            // test results
            return overallResult;
        }
        #endregion

        #region EventArg objects
        public class AvailabilityChangedEventArgs : EventArgs
        {
            public AvailabilityChangedEventArgs(bool availableBefore, bool availableNow)
            {
                AvailableBefore = availableBefore;
                AvailableNow = availableNow;
            }
            public bool AvailableBefore { private set; get; }
            public bool AvailableNow { private set; get; }
        }

        public class BeginTestEventArgs : EventArgs
        {
            public BeginTestEventArgs(bool isConnected = false)
            {
                IsConnected = isConnected;
            }

            // application can set this to true to indicate that it has a working network connection. we can spare our time checking then.
            public bool IsConnected { set; get; }
        }
        #endregion
    }
}
